﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Corecalc
{
    public enum Heuristic { Sarkar, BiggestO_LowestNodeCount, BiggestO_LowestFt, LowestO_LowestFt, LowestO_LowestNodeCount, LowestNodeCount_BiggestO }

    public static partial class Functions
    {
        public static void InitCalls(GRGraph g) {
            foreach (GRNode n in g.Nodes) {
                if (n is BuiltInFunctionCallNode) {

                    BuiltInFunctionCallNode mfn = n as BuiltInFunctionCallNode;
                    HashSet<GRNode> inputEdges = new HashSet<GRNode>();

                    foreach (GRNode fn in mfn.FormulaArgs) {
                        inputEdges.Add(fn);
                    }

                    foreach (GRNode cn in mfn.ConstArgs) {
                        inputEdges.Add(cn);
                    }

                    foreach (GRNode inp in mfn.InputEdges) {
                        inputEdges.Add(inp);
                    }

                    Func<GRNode[], double> timeEstimator = Function.TimeEstimatorFor(mfn.Name);
                    n.Ft = timeEstimator(inputEdges.ToArray());

                } else if (n is ParallelNode) {
                    throw new System.NotImplementedException();
                } else if (n is CompoundNode) {
                    CompoundNode cn = n as CompoundNode;
                    foreach (GRGraph sg in cn.Graphs) {
                        InitCalls(sg);
                    }
                }
            }
        }

        public static void AssignCosts(GRGraph g) {
            foreach (GRNode node in g.Nodes) {
                if (node is ParallelNode) {
                    throw new System.NotImplementedException("...");
                } else if (node is CompoundNode) {
                    CompoundNode cn = node as CompoundNode;
                    foreach (GRGraph sg in cn.Graphs)
                        AssignCosts(sg);

                    cn.Ft = cn.ComputeFt() + 100;
                }
            }

            g.Ft = g.Nodes.Sum(n => n.Ft);
        }

        public static void CostAssignment(GRGraph g) {
            InitCalls(g);
            AssignCosts(g);
        }

        public static Partition MakeInitialPartition(GRGraph g) {
            List<GRNode> pn = InitPrimitiveNodes(g);
            Dictionary<GRNode, Task> tp = new Dictionary<GRNode, Task>();

            foreach (GRNode n in pn) {
                Task task = new Task();
                task.AddNode(n, true);

                tp.Add(n, task);
                SetTaskCosts(task);
            }

            return new Partition(pn, tp);
        }

        public static Partition PartitionFunctions(GRGraph g, out int bestIter) {
            Partition p = MakeInitialPartition(g);
            Tuple<Partition, int, int> result = PartitionGraph(g, p, Heuristic.Sarkar);

            bestIter = result.Item2;
            return result.Item1;
        }

        public static Partition PartitionFunctions_Reconstruct(GRGraph g, Action cpNotifier, int bestIter) {
            Partition p = MakeInitialPartition(g);
            Tuple<Partition, int, int> result = PartitionGraph(g, p, Heuristic.Sarkar, true, bestIter, cpNotifier);

            return result.Item1;
        }

        public static List<GRNode> InitPrimitiveNodes(GRGraph g) {
            List<GRNode> pn = new List<GRNode>();

            foreach (GRNode n in g.Nodes) {
                if (n.Ft < MPModel.TMin) {
                    pn = pn.Union(new List<GRNode> { n }).ToList();

                    if (n is BuiltInFunctionCallNode) {
                        var bfn = n as BuiltInFunctionCallNode;
                        bfn.Par = false;
                    } else if (n is ParallelNode) {
                        var ppn = n as ParallelNode;
                        ppn.Size = (int)Math.Ceiling(ppn.F);
                    }
                } else if (n is BuiltInFunctionCallNode) {
                    var bfn = n as BuiltInFunctionCallNode;
                    pn = pn.Union(new List<GRNode> {n}).ToList();
                    bfn.Par = true;
                } else if (n is SimpleNode) {
                    pn = pn.Union(new List<GRNode> {n}).ToList();
                } else if (n is ParallelNode) {
                    throw new System.NotImplementedException();
                } else {
                    //Compound Node
                    CompoundNode cn = n as CompoundNode;
                    foreach (var sg in cn.Graphs)
                    {
                        List<GRNode> sgpn = InitPrimitiveNodes(sg);
                        pn = pn.Union(sgpn).ToList();
                    }
                    pn = pn.Union(new List<GRNode> {cn}).ToList();
                }
            }

            return pn;
        }

        //Sets the cost of a task after an ACTUAL merge
        public static void SetTaskCosts(Task t) {
            if (t.IsSingleton && t.First is ParallelNode) {
                throw new System.NotImplementedException();
            } else {
                t.O = MPModel.TSched + t.T_in() + t.T_out();
                t.CP = t.T() + t.O;
            }
        }

        //Sets task cost of tasks originated from testing merge candidates
        public static void SetTempTaskCosts(Task t) {
            if (t.IsSingleton && t.First is ParallelNode) {
                throw new System.NotImplementedException();
            } else {
                t.O = MPModel.TSched + t.T_in_Temp() + t.T_out_Temp();
                t.CP = t.T() + t.O;
            }
        }

        public static Tuple<Partition, int, int> PartitionGraph(GRGraph g, Partition p, Heuristic h, bool reconstruct = false, int bestIndex = 0, Action cpNotifier = null) {
            int bestIter = 0; int curIter = 0;
            double overheadSum = p.Tasks.Sum(t => t.Q * t.O);

            SetGraphCP(g, p);   //Not part of the pseudo-code
            if (cpNotifier != null)
                cpNotifier();

            double overheadTerm = 1 + overheadSum / g.Ft;
            double criticalPathTerm = g.CP * MPModel.ProcessorCount / g.Ft;
            double bestCost = p.Cost = Math.Max(criticalPathTerm, overheadTerm);

            double oldOverhead = 0.0;
            bool done = false;
            Task T_new = null;
            List<double> costs = new List<double>();
            List<double> cps = new List<double>();
            List<double> overheads = new List<double>();

            string a = "";

            Console.WriteLine("TASK IN THE START CHIEFLY: " + p.TP.Values.Distinct().Count());

            System.IO.File.Create("LALA.txt").Close();

            while (p.Tasks.Distinct().Count() > 1) {
                var tasks = p.Tasks.Distinct();
                var distinctTasksCount = tasks.Count();
                a += distinctTasksCount + ",";

                if (reconstruct && curIter == bestIndex)
                    break;

                curIter = curIter + 1; done = false;

                var T_a = SelectT_a(p, h);

                if (T_a.IncludesAllNodesIn(T_a.Parent)) {                       //In Sarkar's text, the order of the branches is reversed
                    oldOverhead = T_a.Q * T_a.O;
                    GRNode n_b = T_a.First.Parent.Parent as GRNode;             //If hell breaks loose, then check this
                    p.PN = p.PN.Union(new List<GRNode> { n_b }).ToList();       //Refer to section 2.d.ii in PartitionGraph. Precedence in the set operators?

                    T_new = new Task();
                    T_new.AddNode(n_b, true);
                    p.TP[n_b] = T_new;

                    p.UpdateTaskMapping(T_new);

                    done = true;
                    if (n_b is ParallelNode)
                        (n_b as ParallelNode).Size = 1;
                } else if (T_a.IsSingleton) {
                    oldOverhead = T_a.Q * T_a.O;

                    GRNode n_a = T_a.GetNode(0);
                    if (n_a is BuiltInFunctionCallNode) {
                        BuiltInFunctionCallNode bfn = n_a as BuiltInFunctionCallNode;
                        if (bfn.Par) {
                            bfn.Par = false;
                            done = true;
                            T_new = T_a;
                        }
                    } else if (n_a is ParallelNode) {
                        ParallelNode pn = n_a as ParallelNode;
                        if (2 * pn.Size <= pn.F) {
                            pn.Size = 2 * pn.Size;
                            done = true;
                            T_new = T_a;
                        }
                    }
                }

                if (!done) {
                    Dictionary<GRNode, Task> localtp;
                    Dictionary<Task, HashSet<Task>> path;

                    SetupMerge(T_a, p, out localtp, out path);

                    List<Task> sameParentAsT_a = p.TP.Values.Distinct()
                                                            .Where(t => t != T_a && t.Parent == T_a.Parent)
                                                            .ToList();
                    Task T_b = null;
                    double doesntMatter;
                    double bestCP = double.MaxValue;

                    List<Task> resultTasks = new List<Task>();

                    int candidates = 0;
                    foreach (var T_b_candidate in sameParentAsT_a) {
                        Task resultTask;

                        //if (!(path[T_a].Contains(T_b_candidate) || path[T_b_candidate].Contains(T_a)))
                        //    continue;
                        candidates++;
                        MergeTasks(T_a, T_b_candidate, localtp, false, path, out resultTask, out doesntMatter);
                        SetTempTaskCosts(resultTask);
                        Partition tempPartition = p.Copy(resultTask);
                        tempPartition.UpdateTaskMapping(resultTask);
                        SetCPUntilRoot(resultTask.Parent, resultTask, tempPartition);

                        resultTasks.Add(resultTask);    //Debug

                        if (g.CP < bestCP) {
                            bestCP = g.CP;
                            T_b = T_b_candidate;
                        }
                    }

                    //Merge T_a with the best candidate for T_b
                    MergeTasks(T_a, T_b, localtp, true, path, out T_new, out oldOverhead);
                    p.UpdateTaskMapping(T_new);
                }

                SetTaskCosts(T_new);
                SetCPUntilRoot(T_new.Parent, T_new, p);

                overheadSum = overheadSum - oldOverhead + T_new.Q * T_new.O;
                overheadTerm = 1 + overheadSum / g.Ft;
                criticalPathTerm = g.CP * MPModel.ProcessorCount / g.Ft;

                string biggest = "";
                if (criticalPathTerm > overheadTerm) {
                    p.Cost = criticalPathTerm;
                    biggest = "Critical Path Term";
                } else if (criticalPathTerm < overheadTerm) {
                    p.Cost = overheadTerm;
                    biggest = "Overhead Term";
                } else {
                    p.Cost = criticalPathTerm;
                    biggest = "Two terms are equally big";
                }

                costs.Add(p.Cost);
                cps.Add(g.CP);
                overheads.Add(overheadSum);

                if (p.Cost <= bestCost) {
                    bestIter = curIter;
                    bestCost = p.Cost;
                }

                distinctTasksCount = p.TP.Values.Distinct().Count();
                System.IO.File.AppendAllLines("LALA.txt", new[] {
                    overheadSum + ";" + g.Ft + ";" + g.CP + ";" + distinctTasksCount
                });

                if (!reconstruct) {
                    Console.WriteLine("CurIter: {0}, BestIter: {1}, Tasks: {2} \t\t 1 + overheadSum [{3:0.00}] / g.Ft [{4:0.00}] = {5:0.00} \t\t g.CP [{6:0.00}] * P [{7}] / g.Ft [{8:0.00}] = {9:0.00} \t\t\t Biggest: {10}",
                                                              curIter,
                                                              bestIter,
                                                              p.Tasks.Distinct().Count(),
                                                              1 + overheadSum,
                                                              g.Ft,
                                                              overheadTerm,
                                                              g.CP,
                                                              MPModel.ProcessorCount,
                                                              g.Ft,
                                                              criticalPathTerm,
                                                              biggest);
                } else {
                    Console.WriteLine("Reconstructing: curIter: {0}", curIter);
                }
            }

            if (reconstruct) {
                return new Tuple<Partition, int, int>(p, curIter, curIter);
            } else {
                return new Tuple<Partition, int, int>(p, bestIter, curIter);
            }
        }

        private static Task SelectT_a(Partition p, Heuristic h)
        {
            switch (h)
            {
                case Heuristic.Sarkar:{
                    return p.Tasks.Distinct().OrderBy(t => t.Q * t.O).Last();    
                }
                case Heuristic.BiggestO_LowestFt: {
                    return p.Tasks.Distinct().OrderBy(t => t.Q * t.O).ThenByDescending(t => t.Nodes.Sum(n => n.Ft)).Last();
                }
                case Heuristic.BiggestO_LowestNodeCount: {
                    return p.Tasks.Distinct().OrderBy(t => t.Q * t.O).ThenByDescending(t => t.Nodes.Count).Last();
                }
                case Heuristic.LowestO_LowestFt: {
                    return p.Tasks.Distinct().OrderByDescending(t => t.Q * t.O).ThenByDescending(t => t.Nodes.Sum(n => n.Ft)).Last();
                }
                case Heuristic.LowestO_LowestNodeCount: {
                    return p.Tasks.Distinct().OrderByDescending(t => t.Q * t.O).ThenByDescending(t => t.Nodes.Count).Last();
                }
                case Heuristic.LowestNodeCount_BiggestO: {
                    return p.Tasks.Distinct().OrderByDescending(t => t.Nodes.Count()).ThenBy(t => t.Q * t.O).Last();
                }
                default: return p.Tasks.Distinct().OrderBy(t => t.Q * t.O).Last(); // Equals Heuristic.Sarkar
            }
        }

        private static void SetCPUntilRoot(GRGraph g, Task T_new, Partition p) {
            GRGraph gPrime = T_new.Parent;

            while (true) {
                double oldGPrimeCP = gPrime.CP;
                SetGraphCP(gPrime, p);

                if (gPrime == g || (oldGPrimeCP == gPrime.CP))
                    break;

                GRNode nPrime = gPrime.Parent as GRNode;
                double oldNPrimeCP = nPrime.CP;
                
                SetENCP(nPrime);
                gPrime = nPrime.Parent as GRGraph;

                if (nPrime.CP == oldNPrimeCP)
                    break;
            }
        }

        public static void SetupMerge(Task T_a, Partition p, out Dictionary<GRNode, Task> ltp, out Dictionary<Task, HashSet<Task>> pth) {
            Dictionary<GRNode, Task> localtp = new Dictionary<GRNode, Task>();
            Action<GRNode, Task> addTaskToLocalTp = (node, task) => {
                node.TempTask = task;
                node.Task = task;
                localtp.Add(node, task);
            };

            GRGraph g = T_a.Parent;
            foreach (GRNode n in g.Nodes) {
                if (p.PN.Contains(n)) {
                    addTaskToLocalTp(n, p.TP[n]);
                } else {
                    Task t = new Task();
                    t.AddNode(n, true);

                    addTaskToLocalTp(n, t);
                }
            }

            var path = new Dictionary<Task, HashSet<Task>>();
            foreach (Task t_i in localtp.Values.Distinct()) {
                var reachable = new HashSet<Task>();

                foreach (GRNode node in t_i.Nodes) {
                    node.DepthFirstSearch(n => reachable.Add(n.Task));
                }

                path.Add(t_i, reachable);
            }

            ltp = localtp;
            pth = path;
        }

        public static void MergeTasks(Task t_a, Task t_b, Dictionary<GRNode, Task> localtp, bool final, Dictionary<Task, HashSet<Task>> path, out Task t, out double overhead) {
            Debug.Assert(t_a.Parent == t_b.Parent);

            Task t_new = new Task();
            double oldOverhead = 0.0;

            foreach (Task t_i in localtp.Values.Distinct()) {
                bool b1 = path[t_a].Contains(t_i), b2 = path[t_b].Contains(t_i), b3 = path[t_i].Contains(t_a), b4 = path[t_i].Contains(t_b);

                if ((b1 || b2) && (b3 || b4)) {
                    foreach (GRNode node in t_i.Nodes) {
                        t_new.AddNode(node, final);
                    }

                    oldOverhead = oldOverhead + t_i.Q * t_i.O;

                    if (t_i.Nodes[0] is CompoundNode) {
                        CompoundNode t_i_cmp = t_i.Nodes[0] as CompoundNode;
                        double subOverhead = t_i_cmp.Graphs.SelectMany(g => g.Nodes).Select(n => n.Task).Distinct().Sum(st => st.Q * st.O);
                        oldOverhead += subOverhead;
                    }
                }
            }

            t = t_new;
            overhead = oldOverhead;
        }

        public static void SetGraphCP(GRGraph g, Partition p) {
            //Compute new localtp - different from the localtp in SetupMerge and MergeTasks
            Dictionary<GRNode, Task> localtp = new Dictionary<GRNode, Task>();
            Action<GRNode, Task> addTaskToLocalTp = (sn, t) => {
                sn.TempTask = t;
                sn.Task = t;
                localtp.Add(sn, t);
            };

            foreach (GRNode n in g.Nodes) {
                if (p.PN.Contains(n)) {
                    addTaskToLocalTp(n, p.TP[n]);
                } else {
                    Task t = new Task();
                    t.AddNode(n, true);

                    addTaskToLocalTp(n, t);
                }
            }

            Dictionary<Task, HashSet<Task>> prec = new Dictionary<Task, HashSet<Task>>();
            foreach (GRNode n_a in g.Nodes) {
                foreach (GRNode n_b in g.Nodes) {
                    if (!prec.ContainsKey(n_a.Task))
                        prec.Add(n_a.Task, new HashSet<Task>());

                    if (localtp[n_a] != localtp[n_b] && n_b.InputEdges.Contains(n_a)) {
                        prec[n_a.Task].Add(n_b.Task);
                    }
                }
            }

            List<Task> taskPrecedence = Functions.TopSort(prec.Keys, t => prec[t]);

            taskPrecedence.ForEach(t => t.Start = 0);

            //Order of the task in list "list"
            //Is it OK to assume that any task's start must be equal to the sum of "start" and "cp" of the
            //start lying directly behind it in the list?
            for (int j = 0; j < taskPrecedence.Count(); j++) {
                double endTime = 0.0;
                Task endsLast = null;
                for (int i = 0; i < j; i++) {
                    if (prec[taskPrecedence[i]].Contains(taskPrecedence[j]))
                        if (taskPrecedence[i].Start + taskPrecedence[i].CP > endTime) {
                            endsLast = taskPrecedence[i];
                            endTime = endsLast.Start + endsLast.CP;
                        }
                }

                if (endsLast != null) {
                    taskPrecedence[j].Start = Math.Max(0, endsLast.Start + endsLast.CP);
                }
            }

            g.CP = Math.Max(0, taskPrecedence.Max(t => t.Start + t.CP));
        }

        public static void SetENCP(GRNode n) {
            if (n is ParallelNode) {
                (n as ParallelNode).CP = (n as ParallelNode).Graph.CP;
            } else if (n is CompoundNode) {
                CompoundNode cn = n as CompoundNode;
                cn.CP = cn.Graphs.Sum(g => g.F * g.CP);         //Does g.CriticalPath represent the sum of the CP values of the entire graph or only its own?
            }
        }

        public static IEnumerable<GRNode> TopSort(Dictionary<GRNode, List<GRNode>> outgoing) {
            return Functions.TopSort(outgoing.Keys, n => outgoing[n]);
        }

        public static List<A> TopSort<A>(IEnumerable<A> items, Func<A, IEnumerable<A>> outgoing) {
            List<A> sortedList = new List<A>();
            List<A> marked = new List<A>();
            List<A> tempMarked = new List<A>();

            Action<A> visit = null;
            visit = n => {
                if (tempMarked.Contains(n)) throw new Exception("Uh oh..!!");
                if (!marked.Contains(n)) {
                    tempMarked.Add(n);

                    var o = outgoing(n);
                    foreach (A m in o)
                        visit(m);

                    marked.Add(n);
                    tempMarked.Remove(n);
                    sortedList.Add(n);      //Should've been AddFirst, we reverse the list instead
                }
            };

            Queue<A> unmarked = new Queue<A>(items);
            while (unmarked.Count > 0) {
                A n = unmarked.Dequeue();
                visit(n);
            }

            sortedList.Reverse();
            return sortedList;
        }
    }

    public class Partition
    {
        public List<GRNode> PN { get; set; }
        public Dictionary<GRNode, Task> TP { get; set; }

        public double Cost { get; set; }

        public Partition(List<GRNode> pn, Dictionary<GRNode, Task> tp) {
            PN = pn;
            TP = tp;
        }

        public double T_total() {
            return TP.Values.Distinct().Sum(t => t.Q * (t.T() + t.O));
        }

        public double T_crit() {
            return PN.Sum(n => n.Ft) + Tasks.Distinct().Sum(t => t.O);
        }

        public void UpdateTaskMapping(Task t_new) {
            //if (t_a.IsSingleton && t_a.First is CompoundNode) {
            //    CompoundNode taNode = t_a.First as CompoundNode;
            //    foreach (GRGraph sg in taNode.Graphs) {
            //        PN = PN.Except(sg.Nodes).ToList();

            //        MergeComplexNodeSubGraphs(t_new, sg.Nodes);
            //    }
            //}
            //if (t_b.IsSingleton && t_b.First is CompoundNode) {

            //    CompoundNode tbNode = t_b.First as CompoundNode;
            //    foreach (GRGraph sg in tbNode.Graphs) {
            //        PN = PN.Except(sg.Nodes).ToList();

            //        MergeComplexNodeSubGraphs(t_new, sg.Nodes);
            //    }
            //}

            var containedNodes = t_new.Nodes.Where(n => n is CompoundNode)
                                            .Select(n => n as CompoundNode)
                                            .SelectMany(n => n.ContainedNodes()).ToList();

            RemoveSubgraphs(containedNodes);

            foreach (var node in t_new.Nodes) {
                TP[node] = t_new;
            }
        }

        private void RemoveSubgraphs(List<GRNode> subGraphNodes) {
            PN = PN.Except(subGraphNodes).ToList();
            foreach (var key in subGraphNodes) {
                TP.Remove(key);
            }
        }

        public Partition Copy(Task t_new) {
            Dictionary<GRNode, Task> tempTp = new Dictionary<GRNode, Task>();
            foreach (var key in TP.Keys) {
                if (t_new.Nodes.Contains(key))
                    tempTp.Add(key, t_new);
                else
                    tempTp.Add(key, TP[key]);
            }

            //PN will be modified by UpdateTaskMapping af the call to Copy, so we copy PN, but not its contents
            return new Partition(new List<GRNode>(PN), tempTp);
        }

        public IEnumerable<Task> Tasks {
            get { return TP.Values; }
        }
    }

    public class Task
    {
        public double O { get; set; }
        public double CP { get; set; }
        public double Start { get; set; }

        public List<GRNode> Nodes { get; set; }

        public Task() {
            Nodes = new List<GRNode>();
        }

        public GRNode First {
            get { return Nodes[0]; }
        }

        public bool IsSingleton {
            get { return Nodes.Count == 1; }
        }

        //Q is one in a spreadsheet context
        public double Q {
            get { return 1.0; }
        }

        public double T() {
            return Nodes.Sum(n => n.Ft);
        }

        public GRGraph Parent {
            //All nodes in a task have the same parent
            get { 
                Debug.Assert(Nodes[0].Parent is GRGraph); 
                return Nodes[0].Parent as GRGraph; 
            }
        }

        public bool IncludesAllNodesIn(GRGraph graph) {
            var taskNodes = Nodes;
            var exceptNodes = graph.Nodes.Except(taskNodes);
            return exceptNodes.Count() == 0;
        }

        public double T_in() {
            return Nodes.SelectMany(n => n.InputEdges)
                        .Where(n => n.Task != this)
                        .Distinct()
                        .Sum(n => MPModel.Rc(n.T_out));         //Described in page 75 (reader), definition of E_in
        }

        public double T_out() {
            return Nodes.Where(n => n.OutputEdges.Any(oe => oe.Task != this))
                        .Sum(n => MPModel.Wc(n.T_out));
        }

        public double T_in_Temp() {
            return Nodes.SelectMany(n => n.InputEdges)
                        .Where(n => n.TempTask != this)
                        .Distinct()
                        .Sum(n => MPModel.Rc(n.T_out));         //Described in page 75 (reader), definition of E_in
        }

        public double T_out_Temp() {
            return Nodes.Where(n => n.OutputEdges.Any(oe => oe.TempTask != this))
                        .Sum(n => MPModel.Wc(n.T_out));
        }

        public void AddNode(GRNode n, bool actualMerge) {
            Debug.Assert(!Nodes.Contains(n));

            if (actualMerge) {
                n.Task = this;

                var parents = Nodes.Select(nn => nn.Parent).Distinct();
                if (parents.Count() > 0 && !parents.Contains(n.Parent)) {
                    throw new Exception("");
                }
            }

            n.TempTask = this;
            Nodes.Add(n);
        }

        public GRNode GetNode(int i) {
            return Nodes[i];
        }

        public A FoldNodes<A>(A init, Func<A, GRNode, A> f) {
            A a = init;
            foreach (GRNode node in Nodes) {
                a = f(a, node);
            }
            return a;
        }

        public override string ToString() {
            return string.Format("Task [HashCode: {0}, Nodes: {1}, Start: {2}, O: {3}, CP: {4}]", this.GetHashCode(), Nodes.Count(), Start, O, CP);
        }
    }
}
